package com.zyfdroid.backupftpserver.ftplets;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import androidx.documentfile.provider.DocumentFile;
import com.zyfdroid.backupftpserver.LastModifiedStorageHelper;
import com.zyfdroid.backupftpserver.SAFHelper;
import org.apache.ftpserver.ftplet.FtpFile;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

public class SAFFileSystemManager implements FtpFile{

    private Context ctx;
    private ContentResolver cr;
    private LastModifiedStorageHelper lmHelper;
    private SAFHelper safHelper;

    public SAFFileSystemManager(Context ctx){
        this.ctx = ctx;
        this.cr = ctx.getContentResolver();
        this.safHelper = new SAFHelper(ctx);
        this.lmHelper = new LastModifiedStorageHelper(ctx);

    }

    private String mnt2saf(String mntPath) throws FileNotFoundException{
        if(!mntPath.startsWith("/extra/")){
            throw new FileNotFoundException("cannot cast non mnt path to saf");
        }
        String splitedPath = mntPath.substring("/extra/".length());
        String[] parts = splitedPath.split(Pattern.quote("/"),2);
        String displayName = parts[0];
        String subPath = "/";
        if(parts.length > 1){
            subPath = parts[1];
        }
        String root = safHelper.getByDisplayName(displayName).uri;
        if(root == null){
            throw new FileNotFoundException("SAF not registered");
        }
        root = DocumentFile.fromTreeUri(ctx,Uri.parse(root)).getUri().toString();

        subPath = trimDirectorySeparateChar(subPath);
        if(!root.endsWith("%2f") && !root.endsWith("%2F")){
            root += "%2F";
        }
        root += Uri.encode(subPath);
        if(root.endsWith("%2f") || root.endsWith("%2F")){
            root = root.substring(0,root.length() - 3);
        }
        return root;
    }

    private DocumentFile mnt2file(String mntPath) throws FileNotFoundException {
        return saf2file(mnt2saf(mntPath));
    }

    private DocumentFile saf2file(String safPath){
        return DocumentFile.fromTreeUri(ctx,Uri.parse(safPath));
    }

    private void TryCreateNewFile(String mntPath) {
        String[] parts = trimDirectorySeparateChar(mntPath).split(Pattern.quote("/"));
        String currentDir = "/extra/";
        currentDir = pathCombine(currentDir,parts[1]);
        DocumentFile lastDocumentFile;
        try {
            lastDocumentFile = mnt2file(currentDir);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        for (int i = 2; i < parts.length; i++) {
            currentDir = pathCombine(currentDir,parts[i]);
            try {
                if(!isFileOrDirectoryExists(currentDir)){
                    if(i != parts.length - 1){
                        lastDocumentFile.createDirectory(parts[i]);
                    }
                    else{
                        lastDocumentFile.createFile("application/octet-stream",parts[i]);
                    }
                }
                lastDocumentFile = mnt2file(currentDir);
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            }

        }
    }

    private void TryCreateNewDirectory(String mntPath){
        String[] parts = trimDirectorySeparateChar(mntPath).split(Pattern.quote("/"));
        String currentDir = "";
        currentDir = pathCombine(currentDir,parts[0]);
        currentDir = pathCombine(currentDir,parts[1]);
        DocumentFile lastDocumentFile;
        try {
            lastDocumentFile = mnt2file(currentDir);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        for (int i = 2; i < parts.length; i++) {
            currentDir = pathCombine(currentDir,parts[i]);
            try {
                if(!isFileOrDirectoryExists(currentDir)){
                    lastDocumentFile.createDirectory(parts[i]);
                }
                lastDocumentFile = mnt2file(currentDir);
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            }

        }
    }

    private String getFileName(String mntPath){
        String[] parts = trimDirectorySeparateChar(mntPath).split(Pattern.quote("/"));
        return parts[parts.length - 1];
    }

    public SAFFtpFile2 getByPath(String mntPath) throws FileNotFoundException {
        DocumentFile df = mnt2file(mntPath);
        boolean exists = df.exists();
        boolean isDirectory = df.isDirectory();
        String displayName = getFileName(mntPath);
        long lastModified = lmHelper.getLastModifiedByUri(df);
        String treeUri = df.getUri().toString();
        return new SAFFtpFile2(this,treeUri,mntPath,displayName,isDirectory,lastModified,exists,df.length());
    }

    private List<SAFFtpFile2> fastListFile(String mntPath) throws FileNotFoundException {
        if(!isFileOrDirectoryExists(mntPath)){
            return new ArrayList<>();
        }
        DocumentFile df = mnt2file(mntPath);
        if(!df.isDirectory()){
            throw new FileNotFoundException();
        }
        Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(df.getUri(),DocumentsContract.getDocumentId(df.getUri()));
        String[] requiredColumns = new String[]{
                DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                DocumentsContract.Document.COLUMN_MIME_TYPE,
                DocumentsContract.Document.COLUMN_LAST_MODIFIED,
                DocumentsContract.Document.COLUMN_SIZE
        };
        List<SAFFtpFile2> files = new ArrayList<>();
        try(Cursor c = cr.query(childrenUri,requiredColumns,null,null,null)){
            while (c.moveToNext()){
                String displayName = c.getString(0);
                boolean isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(c.getString(1));
                long size = c.getLong(3);
                String fullPath = pathCombine(mntPath,displayName);
                String treeUri = mnt2saf(fullPath);
                long orignalLm = c.getLong(2);
                long lastModified = lmHelper.getLastModifiedByUri(treeUri,orignalLm);
                SAFFtpFile2 f2 = new SAFFtpFile2(this,treeUri,fullPath,displayName,isDirectory,lastModified,true,size);
                files.add(f2);
            }

        }
        return files;
    }

    private String pathCombine(String... parts){
        String prefix = (parts.length > 0 && parts[0].startsWith("/")) ? "/" : "";
        StringBuilder sb = new StringBuilder();
        sb.append(prefix);
        boolean isFirst = true;
        for (String str :
                parts) {
            if(!isFirst){
                sb.append("/");

            }
            else{
                isFirst = false;
            }
            sb.append(trimDirectorySeparateChar(str));
        }
        return sb.toString();
    }

    private boolean isFileOrDirectoryExists(String mntPath) throws FileNotFoundException {
        return mnt2file(mntPath).exists();
    }

    private String trimDirectorySeparateChar(String str){
        while(str.startsWith("/")){
            str = str.substring(1);
        }
        while (str.endsWith("/")){
            str = str.substring(0,str.length() - 1);
        }
        return str;
    }

    private String getDirectoryName(String mntPath){
        mntPath = trimDirectorySeparateChar(mntPath);
        int i = mntPath.lastIndexOf("/");
        if(i < 0){
            return "/"+mntPath;
        }
        mntPath = mntPath.substring(0,i);
        return "/" + mntPath;
    }

    @Override
    public List<? extends FtpFile> listFiles() {
        List<SAFFtpFile2> files = new ArrayList<>();
        for (SAFHelper.SAFEntry entry :
                safHelper.getAllSafs()) {
            try {
                SAFFtpFile2 file = getByPath("/extra/"+entry.displayName);
                files.add(file);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
        return files;
    }

    // region Ftp File Implementations

    @Override
    public String getAbsolutePath() {
        return "/extra";
    }

    @Override
    public String getName() {
        return "extra";
    }

    @Override
    public boolean isHidden() {
        return false;
    }

    @Override
    public boolean isDirectory() {
        return true;
    }

    @Override
    public boolean isFile() {
        return false;
    }

    @Override
    public boolean doesExist() {
        return true;
    }

    @Override
    public boolean isReadable() {
        return true;
    }

    @Override
    public boolean isWritable() {
        return false;
    }

    @Override
    public boolean isRemovable() {
        return false;
    }

    @Override
    public String getOwnerName() {
        return "android";
    }

    @Override
    public String getGroupName() {
        return "android";
    }

    @Override
    public int getLinkCount() {
        return 1;
    }

    @Override
    public long getLastModified() {
        return 0;
    }

    @Override
    public boolean setLastModified(long l) {
        return false;
    }

    @Override
    public long getSize() {
        return 0;
    }

    @Override
    public Object getPhysicalFile() {
        return null;
    }

    @Override
    public boolean mkdir() {
        return false;
    }

    @Override
    public boolean delete() {
        return false;
    }

    @Override
    public boolean move(FtpFile ftpFile) {
        return false;
    }

    @Override
    public OutputStream createOutputStream(long l) throws IOException {
        return null;
    }

    @Override
    public InputStream createInputStream(long l) throws IOException {
        return null;
    }

    public void close() {
        lmHelper.close();
    }

    // endregion

    public static class SAFFtpFile2 implements FtpFile{

        public SAFFtpFile2(SAFFileSystemManager manager, String treeUri, String mntPath, String displayName, boolean isDirectory, long lastModified,boolean exists,long size) {
            this.manager = manager;
            this.treeUri = treeUri;
            this.mntPath = mntPath;
            this.displayName = displayName;
            this.isDirectory = isDirectory;
            this.lastModified = lastModified;
            this.exists = exists;
            this.size = size;
        }

        private SAFFileSystemManager manager;
        private String treeUri;
        private String mntPath;
        private String displayName;
        private boolean isDirectory;
        private long lastModified;
        private boolean exists;
        private long size = -1;

        @Override
        public String getAbsolutePath() {
            return mntPath;
        }

        @Override
        public String getName() {
            return displayName;
        }

        @Override
        public boolean isDirectory() {
            return exists && (isDirectory);
        }

        @Override
        public boolean isFile() {
            return exists && (!isDirectory);
        }

        @Override
        public boolean doesExist() {
            return exists;
        }


        @Override
        public long getLastModified() {
            if(!exists){
                return -1;
            }
            if(lastModified == -1){
                lastModified = manager.lmHelper.getLastModifiedByUri(DocumentFile.fromTreeUri(manager.ctx,Uri.parse(treeUri)));
            }
            return lastModified;
        }

        @Override
        public boolean setLastModified(long l) {
            this.lastModified = l;
            manager.lmHelper.setLastModifiedByUri(DocumentFile.fromTreeUri(manager.ctx,Uri.parse(treeUri)),l);
            return true;
        }

        @Override
        public long getSize() {
            if(exists && !isDirectory){
                if(size == -1){
                    try {
                        size  = manager.mnt2file(mntPath).length();
                    } catch (FileNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                }
                return size;
            }
            return -1;
        }



        @Override
        public boolean mkdir() {
            manager.TryCreateNewDirectory(mntPath);
            lastModified = -1;
            size = -1;
            return true;
        }

        @Override
        public boolean delete() {
            try {
                if(exists) {
                    manager.mnt2file(mntPath).delete();
                }
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            }
            exists = false;
            lastModified = -1;
            size = -1;
            return true;
        }

        @Override
        public boolean move(FtpFile ftpFile) {
            String newMntPath = ftpFile.getAbsolutePath();
            String newMntDirectory = manager.getDirectoryName(newMntPath);
            String oldMntDirectory = manager.getDirectoryName(mntPath);
            try {
                if(manager.isFileOrDirectoryExists(newMntPath)){
                    throw new FileNotFoundException("File already exists");
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
            if(newMntDirectory.toLowerCase().equals(oldMntDirectory.toLowerCase())){
                try {
                    manager.mnt2file(mntPath).renameTo(ftpFile.getName());
                    long oldLm = lastModified;
                    manager.lmHelper.deleteLastModifiedByUri(manager.mnt2file(mntPath));
                    manager.lmHelper.setLastModifiedByUri(manager.mnt2file(newMntPath),oldLm);
                    exists = false;
                    size  = -1;
                    lastModified = -1;
                    isDirectory = false;
                    return true;
                } catch (FileNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
            return false;
        }

        @Override
        public List<? extends FtpFile> listFiles() {
            try {
                return manager.fastListFile(mntPath);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
            catch (Exception ex){
                ex.printStackTrace();
                throw ex;
            }
        }

        @Override
        public OutputStream createOutputStream(long l) throws IOException {
            size = -1;
            lastModified = -1;
            if(!exists){
                manager.TryCreateNewFile(mntPath);
                exists = true;
            }
            return manager.cr.openOutputStream(manager.mnt2file(mntPath).getUri());
        }

        @Override
        public InputStream createInputStream(long l) throws IOException {
            return manager.cr.openInputStream(manager.mnt2file(mntPath).getUri());
        }

        // region Unsupported FS Features
        @Override
        public boolean isHidden() {
            return false;
        }
        @Override
        public boolean isReadable() {
            return true;
        }

        @Override
        public boolean isWritable() {
            return true;
        }

        @Override
        public boolean isRemovable() {
            return true;
        }

        @Override
        public String getOwnerName() {
            return "android";
        }

        @Override
        public String getGroupName() {
            return "android";
        }

        @Override
        public int getLinkCount() {
            return 1;
        }
        @Override
        public Object getPhysicalFile() {
            return treeUri;
        }
        // endregion

    }
}
